package pl.edu.agh.ki.neuralnetwork.builder;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;

import pl.edu.agh.ki.neuralnetwork.exceptions.NeuronNotConnectedException;
import pl.edu.agh.ki.neuralnetwork.exceptions.WrongArgException;
import pl.edu.agh.ki.neuralnetwork.layer.KohonenLayer;
import pl.edu.agh.ki.neuralnetwork.layer.Layer;
import pl.edu.agh.ki.neuralnetwork.layer.SimpleLayer;
import pl.edu.agh.ki.neuralnetwork.network.NeuralNetwork;
import pl.edu.agh.ki.neuralnetwork.network.NeuralNetworkImpl;
import pl.edu.agh.ki.neuralnetwork.neurons.InnerNeuron;
import pl.edu.agh.ki.neuralnetwork.neurons.InputNeuron;
import pl.edu.agh.ki.neuralnetwork.neurons.InputNeuronImpl;
import pl.edu.agh.ki.neuralnetwork.neurons.KohonenNeuron;
import pl.edu.agh.ki.neuralnetwork.neurons.Neuron;

public class KohonenNetworkBuilder implements NeuralNetworkBuilder {
	private String path;
	private double minRand;
	private double maxRand;
	private int learingIteartions;
	private int initMode;
	private int colsCount;
	private int rowsCount;
	private int inputLayerSize;
	private KohonenLayer kohonenLayer;
	private int learningSamplesCount;
	private List<List<Double>> learningInput;
	private int toRecogniezeCount;
	private List<List<Double>> toRecognizeInput;
	private SimpleLayer<InputNeuron> inputLayer;
	private int neighbourhood;
	private int neighbourhoodRadius;

	public KohonenNetworkBuilder(String path) {
		this.path = path;
	}

	/**
	 * function omits line begins with #
	 * 
	 * @param br
	 * @return
	 * @throws IOException
	 */
	private String getNextLine(BufferedReader br) throws IOException {
		String t;
		while ((t = br.readLine()).charAt(0) == '#')
			;
		return t;
	}

	public NeuralNetwork build() throws Exception {
		List<Layer<InnerNeuron>> innerLayers = new LinkedList<Layer<InnerNeuron>>();
		inputLayer = new SimpleLayer<InputNeuron>();

		// reading parameters from file
		System.out.println((new File(path)).getAbsolutePath());
		BufferedReader br = new BufferedReader(new FileReader(new File(path)));
		// read max Rand
		minRand = Double.parseDouble(getNextLine(br));
		// read min Rand
		maxRand = Double.parseDouble(getNextLine(br));
		// read innerLayerSize
		inputLayerSize = Integer.parseInt(getNextLine(br));
		// read kohonenLayer rows count
		rowsCount = Integer.parseInt(getNextLine(br));
		// read kohonen Layer cols count
		colsCount = Integer.parseInt(getNextLine(br));
		// read weights initiation mode
		initMode = Integer.parseInt(getNextLine(br));
		// read neighbourhood mode
		neighbourhood = Integer.parseInt(getNextLine(br));
		//read neighbourhood radius
//		neighbourhoodRadius = Integer.parseInt(getNextLine(br));
		// read learing iterations
		learingIteartions = Integer.parseInt(getNextLine(br));
		// read samples count
		learningSamplesCount = Integer.parseInt(getNextLine(br));
		// read learingn input
		learningInput = new ArrayList<List<Double>>();
		for (int j = 0; j < learningSamplesCount; j++) {
			learningInput.add(new ArrayList<Double>(inputLayerSize));

			for (int i = 0; i < Math.sqrt(inputLayerSize); i++) {
				String[] line = getNextLine(br).split(" ");
				for (String s : line) {
					learningInput.get(j).add(Double.parseDouble(s));
				}
			}
		}
		// read to recognize count
		toRecogniezeCount = Integer.parseInt(getNextLine(br));
		// read to recognize input
		toRecognizeInput = new ArrayList<List<Double>>();
		for (int j = 0; j < toRecogniezeCount; j++) {
			toRecognizeInput.add(new ArrayList<Double>(inputLayerSize));

			for (int i = 0; i < Math.sqrt(inputLayerSize); i++) {
				String[] line = getNextLine(br).split(" ");
				for (String s : line) {
					toRecognizeInput.get(j).add(Double.parseDouble(s));
				}
			}
		}
		// end of reading parameters

		// creating network

		int kohonenLayerSize = rowsCount * colsCount;
		for (int i = 0; i < inputLayerSize; i++) {
			inputLayer.add(new InputNeuronImpl());
		}

		
		kohonenLayer = new KohonenLayer(rowsCount, colsCount, inputLayerSize,neighbourhood,neighbourhoodRadius);

		Random rand = new Random();
		for (int i = 0; i < kohonenLayerSize; i++) {

			List<Double> weights = new ArrayList<Double>(inputLayerSize);
			if (initMode == 0) {
				for (int j = 0; j < inputLayerSize; j++)
					weights.add(0.0d);
			} else if (initMode == 1) {
				for (int j = 0; j < inputLayerSize; j++){
					double t=rand.nextDouble();
					t=(maxRand-minRand)*t+minRand;
					weights.add(t);
				}
			}

			KohonenNeuron kohonenNeuron = new KohonenNeuron(inputLayer
					.getNeuronsList(), weights);
			kohonenLayer.add(kohonenNeuron);
		}
		kohonenLayer.setNeighbourHood();
		innerLayers.add(kohonenLayer);
		return new NeuralNetworkImpl(inputLayer, innerLayers);
	}

	/**
	 * funciont says if two neurons are neighbours
	 * 
	 * @param a
	 * @param b
	 * @return 1 for true, 0 for false
	 */
	private int G(KohonenNeuron a, KohonenNeuron b) {
		if (a.isNeighBour(b))
			return 1;
		else
			return 0;
	}

	/**
	 * function returns learning coefficient (it should be decreasing)
	 * 
	 * @param iteration
	 * @return
	 */
	private double n(int iteration) {
		if (iteration < 1000)
			return 1.0;
		else if (iteration < 2000)
			return 0.5;
		else if (iteration < 3000)
			return 0.25;
		else if (iteration < 4000)
			return 0.125;
		else
			return 0.062;
	}

	/**
	 * function search for a winner
	 * 
	 * @param layer
	 * @param inputVector
	 * @return
	 * @throws WrongArgException
	 */
	private InnerNeuron findWinner(KohonenLayer layer, List<Double> inputVector)
			throws WrongArgException {
		InnerNeuron winner = null;
		double min = Double.MAX_VALUE;
		for (InnerNeuron innerNeuron : layer) {
			((KohonenNeuron) innerNeuron).setDist(inputVector);
		}
		for (InnerNeuron innerNeuron : layer) {
			KohonenNeuron kn = (KohonenNeuron) innerNeuron;
			if (kn.getDist() < min) {
				min = kn.getDist();
				winner = innerNeuron;
			}
		}
		return winner;
	}


	/**
	 * function learns network
	 * 
	 * @param iteration
	 * @throws WrongArgException
	 */
	private void learn(int iteration, List<Double> input)
			throws WrongArgException {
		KohonenNeuron winner = (KohonenNeuron) findWinner(kohonenLayer, input);
		for (Neuron neuron : kohonenLayer.getNeuronsList()) {
			KohonenNeuron kn = (KohonenNeuron) neuron;

			for (int i = 0; i < inputLayerSize; i++) {
				try {
					double w = kn.getWeight(inputLayer.get(i));
					double w1 = w + n(iteration) * G(winner, kn)
							* (input.get(i) - w);
					kn.setWeight(inputLayer.get(i), w1);
				} catch (NeuronNotConnectedException e) {
					e.printStackTrace();
				}
			}

		}
	}
	private List<Double> normalize(List<Double> input){
		List<Double> normalizedList = new ArrayList<Double>(input.size());
		for(int j=0;j<input.size();j++){
			double sum=0.0d;
			for(int i=0;i<input.size();i++){
				sum+=input.get(i)*input.get(i);
			}
			sum = Math.sqrt(sum);
			
			normalizedList.add(input.get(j)/sum);
		}
		return normalizedList;
	}
	private KohonenNeuron recognize(List<Double> input)
			throws WrongArgException {

		for (int i = 0; i < input.size(); i++) {
			if (i % (int) Math.sqrt(input.size()) == 0)
				System.out.print("\n");
			System.out.print(input.get(i) + " ");
		}
		System.out.println();
		return (KohonenNeuron) findWinner(kohonenLayer, input);
	}

	public static void main(String[] args) {
		KohonenNetworkBuilder knb = new KohonenNetworkBuilder("kohonen.txt");
		try {
			knb.build();
			
			System.out.println("przed uczeniem warstwa kohonena");
			System.out.println(knb.kohonenLayer);
			for (int i = 0; i < knb.learingIteartions; i++)
				for (int j = 0; j < knb.learningSamplesCount; j++) 
					knb.learn(i, knb.learningInput.get(j));
			
			System.out.println("po uczeniu warstwa kohonena");
			System.out.println(knb.kohonenLayer);

			System.out.println("rozpoznawanie");
			for (List<Double> input : knb.toRecognizeInput) {
				KohonenNeuron kn = knb.recognize(input);
				System.out.println("jako " + kn);

			}
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}

}
